Flutter(dart)でimmutableなクラスを生成するfreezedについてまとめてみた

Flutter(dart)でimmutableなクラスを生成するfreezedについてまとめてみた

Clock Icon2024.07.30

こんにちは、ゲームソリューション部のsoraです。
今回は、Flutterでimmutableなクラスを生成するfreezedについて書いていきます。
書き方を忘れることが多いため、これを機に自分なりにまとめようと思います。

immutableなクラスとは

immutableなクラスから作成したオブジェクトの値を変更できないクラス
値が変更されないため、同時にアクセスされた場合にも値のずれが起こらない・意図せず値が変更されないなどのメリットがあります。
値を変える必要のある場合以外は、基本的にはimmutableで良さそうかなと思いました。

利用する主要なパッケージ

freezed
Flutter(Dart)でimmutableなクラスを自動生成するパッケージ
json形式の変換も簡単に実装可能
https://pub.dev/packages/freezed

freezed_annotation
freezedのアノテーションを使うためのパッケージ
@freezed@JsonSerializableを使うために必要
https://pub.dev/packages/freezed_annotation

build_runner
Dartにてコード生成を行う際に使用するパッケージ
freezedでコードの自動生成を行うために必要
https://pub.dev/packages/build_runner

json_annotation
json_serializable
Dartにてjsonの変換を簡単に行ったり、コードを自動生成する際に必要なパッケージ
freezedのクラスにて、jsonを扱う際に必要
https://pub.dev/packages/json_annotation
https://pub.dev/packages/json_serializable

pubspec.yaml

上記パッケージを使用するためのpubspec.yamlを、一部抜粋して以下参考として記載します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6
  connectivity_plus: ^6.0.3
  freezed_annotation: ^2.4.4
  json_annotation: ^4.9.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  build_runner: ^2.4.11
  freezed: ^2.5.2
  json_serializable: ^6.8.0

freezedの使い方

テンプレート

Person(person)の部分をそれぞれ書き換えて利用します。

person.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';
part 'person.g.dart';


class Person with _$Person {
  const factory Person({
    required String name,
    required int age,
  }) = _Person;

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

テンプレートの解説

テンプレートの解説については、以下コメント内に記載しています。

import 'package:freezed_annotation/freezed_annotation.dart';

// freezedで自動生成されるファイル
part 'person.freezed.dart';
part 'person.g.dart';

// freezedでのクラスの作成

// Personクラスの作成、Personクラスをプライベートクラスである_Personクラスに委譲している
class Person with _$Person {
  const factory Person({
    required String name,
    required int age,
  }) = _Person;

  // json形式の変換
  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

json形式の変換が不要の場合は、part 'person.g.dart';やjson形式の変換用のコード部分は不要です。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';


class Person with _$Person {
  const factory Person({
    required String name,
    required int age,
  }) = _Person;
}

クラスのプロパティについて

freezedクラスのプロパティについて、以下にそれぞれ記載します。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';
part 'person.g.dart';


class Person with _$Person {
    // @Asset:条件を満たさない場合に、エラーメッセージを出してインスタンスを生成しない
    ('name.isNotEmpty', 'Name cannot be empty')

    const factory Person({
      // required:必須(Nullを許容しない)
      required String name,
      // ?:Nullを許容
      int? age,
      // @Default:Nullだった場合のデフォルト値を指定
      (00000000000) phoneNumber,
      // @JsonKey:jsonのキーとプロパティ名が異なる場合の紐づけ
      (name: 'e_mail') required String email,
    }) = _Person;

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

メソッドの追加

freezedクラスの中でメソッドを定義することも可能です。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';


abstract class Person with _$Person {
  // カスタムメソッドを追加するためのプライベートコンストラクタの定義
  const Person._();
  const factory Person({
    required String name,
    required int age,
  }) = _Person;

  // カスタムメソッド
  void priName() {
    print('$name');
  }
}

freezedのクラス作成

コードを自動生成する際は、以下コマンドを実行します。

# コード生成
flutter pub run build_runner build

# コード生成(オプションあり)
# ⇒既存の生成ファイルと新しく生成されるファイルが競合した場合に、オプションなしだとエラーが出る場合がある
# ⇒その場合は、オプションを付けることで既存のファイルを削除して生成する
flutter pub run build_runner build --delete-conflicting-outputs

jsonの読み取り

freezedで生成したクラスを利用する方法について、まずリスト化されていないjsonの場合は以下です。

// 非リストの場合
const jsonString = '''
{
  "name": "John Doe",
  "age": 30,
  "phoneNumber": 1234567890,
  "e_mail": "john.doe@example.com"
}
''';
Person person = Person.fromJson(json.decode(jsonString));

リスト化されているjsonの場合は以下です。

// リストの場合
const jsonString = '''
[
  {
    "name": "John Doe",
    "age": 30,
    "phoneNumber": 1234567890,
    "e_mail": "john.doe@example.com"
  },
  {
    "name": "Jane Smith",
    "age": 25,
    "phoneNumber": 9876543210,
    "e_mail": "jane.smith@example.com"
  }
]
''';
List<dynamic> jsonData = json.decode(jsonString);
List<Person> person = jsonData.map((data) => Person.fromJson(data)).toList();

サンプルコード

説明は以上ですが、これまでの項目をある程度盛り込んだサンプルコードと実行時の画面も記載しておきます。
sr-flutter-freezed

lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'model/person.dart';

void main() {
  runApp(
      const ProviderScope(
          child: MyApp()
      )
  );
}
class MyApp extends StatelessWidget   {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlueAccent),
          useMaterial3: true,
          fontFamily: 'Noto Sans JP'
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // // 非リストの場合
    // const jsonString = '''
    // {
    //   "name": "John Doe",
    //   "age": 30,
    //   "phoneNumber": 1234567890,
    //   "e_mail": "john.doe@example.com"
    // }
    // ''';
    // Person person = Person.fromJson(json.decode(jsonString));

    // リストの場合
    const jsonString = '''
    [
      {
        "name": "John Doe",
        "age": 30,
        "phoneNumber": 2345678901,
        "e_mail": "john.doe@example.com"
      },
      {
        "name": "Jane Smith",
        "age": 25,
        "e_mail": "jane.smith@example.com"
      }
    ]
    ''';
    List<dynamic> jsonData = json.decode(jsonString);
    List<Person> person = jsonData.map((data) => Person.fromJson(data)).toList();
    final methodTest = person[0].doubleAge();

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('freezedテスト'),
      ),
      body: Center(
        child: Column(
          children: [
            // 非リストの場合
            // Text('Name: ${person.name}'),
            // Text('Age: ${person.age}'),
            // Text('Phone: ${person.phoneNumber}'),
            // Text('email: ${person.email}')

            // リストの場合
            const Text('一人目'),
            Text('Name: ${person[0].name}'),
            Text('Age: ${person[0].age}'),
            Text('Phone: ${person[0].phoneNumber}'),
            Text('email: ${person[0].email}'),
            Text('Age*2: $methodTest'),
            const SizedBox(height: 30,),
            const Text('二人目'),
            Text('Name: ${person[1].name}'),
            Text('Age: ${person[1].age}'),
            Text('Phone: ${person[1].phoneNumber}'),
            Text('email: ${person[1].email}'),
          ],
        ),
      ),
    );
  }
}
model/person.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';
part 'person.g.dart';


abstract class Person with _$Person {
  ('name.isNotEmpty', 'Name cannot be empty')

  const Person._();
  const factory Person({
    required String name,
    required int age,
    (01234567890) int? phoneNumber,
    (name: 'e_mail') required String email,
  }) = _Person;

  int doubleAge() {
    return age * 2;
  }

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

参考

https://zenn.dev/sakusin/articles/b19e9a2c3829e0
https://qiita.com/taniguchi-kyoichi/items/ff888ae4bd6f979c3067
https://dev.classmethod.jp/articles/flutter_freezed_introduction/

最後に

今回は、Flutterでimmutableなクラスを生成するfreezedについて記事にしました。
どなたかの参考になると幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.